/*!
 * KeyboardJS
 * 
 * Copyright 2011, Robert William Hurst
 * Licenced under the BSD License.
 * See https://raw.github.com/RobertWHurst/KeyboardJS/master/license.txt
 */
(function(context, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(factory);
    } else {
        // Browser globals
        context.k = context.KeyboardJS = factory();
    }
}(this, function() {

    //polyfills for ms's peice o' shit browsers
    function bind(target, type, handler) { if (target.addEventListener) { target.addEventListener(type, handler, false); } else { target.attachEvent("on" + type, function(event) { return handler.call(target, event); });  } }
    [].indexOf || (Array.prototype.indexOf = function(a, b, c) { for (c = this.length, b = (c + ~~b) % c; b < c && (!(b in this) || this[b] !== a); b++); return b ^ c ? b : -1; });

    //locals
    var locals = {
        'us': {
            "backspace": 8,
            "tab": 9,
            "enter": 13,
            "shift": 16,
            "ctrl": 17,
            "alt": 18,
            "pause": 19,
            "break": 19,
            "capslock": 20,
            "escape": 27,
            "esc": 27,
            "space": 32,
            "spacebar": 32,
            "pageup": 33,
            "pagedown": 34,
            "end": 35,
            "home": 36,
            "left": 37,
            "up": 38,
            "right": 39,
            "down": 40,
            "insert": 45,
            "delete": 46,
            "0": 48,
            "1": 49,
            "2": 50,
            "3": 51,
            "4": 52,
            "5": 53,
            "6": 54,
            "7": 55,
            "8": 56,
            "9": 57,
            "a": 65,
            "b": 66,
            "c": 67,
            "d": 68,
            "e": 69,
            "f": 70,
            "g": 71,
            "h": 72,
            "i": 73,
            "j": 74,
            "k": 75,
            "l": 76,
            "m": 77,
            "n": 78,
            "o": 79,
            "p": 80,
            "q": 81,
            "r": 82,
            "s": 83,
            "t": 84,
            "u": 85,
            "v": 86,
            "w": 87,
            "x": 88,
            "y": 89,
            "z": 90,
            "meta": 91,
            "command": 91,
            "windows": 91,
            "win": 91,
            "_91": 92,
            "select": 93,
            "num0": 96,
            "num1": 97,
            "num2": 98,
            "num3": 99,
            "num4": 100,
            "num5": 101,
            "num6": 102,
            "num7": 103,
            "num8": 104,
            "num9": 105,
            "multiply": 106,
            "add": 107,
            "subtract": 109,
            "decimal": 110,
            "divide": 111,
            "f1": 112,
            "f2": 113,
            "f3": 114,
            "f4": 115,
            "f5": 116,
            "f6": 117,
            "f7": 118,
            "f8": 119,
            "f9": 120,
            "f10": 121,
            "f11": 122,
            "f12": 123,
            "numlock": 144,
            "num": 144,
            "scrolllock": 145,
            "scroll": 145,
            "semicolon": 186,
            "equal": 187,
            "equalsign": 187,
            "comma": 188,
            "dash": 189,
            "period": 190,
            "slash": 191,
            "forwardslash": 191,
            "graveaccent": 192,
            "openbracket": 219,
            "backslash": 220,
            "closebracket": 221,
            "singlequote": 222
        }

        //If you create a new local please submit it as a pull request or post it in the issue tracker at
        // http://github.com/RobertWhurst/KeyboardJS/issues/
    }

    //keys
    var keys = locals['us'],
        activeKeys = [],
        activeBindings = {},
        keyBindingGroups = [];

    //adds keys to the active keys array
    bind(document, "keydown", function(event) {

        //lookup the key pressed and save it to the active keys array
        for (var key in keys) {
            if (keys.hasOwnProperty(key) && event.keyCode === keys[key]) {
                if (activeKeys.indexOf(key) < 0) {
                    activeKeys.push(key);
                }
            }
        }

        //execute the first callback the longest key binding that matches the active keys
        return executeActiveKeyBindings(event);

    });

    //removes keys from the active array
    bind(document, "keyup", function(event) {

        //lookup the key released and prune it from the active keys array
        for (var key in keys) {
            if (keys.hasOwnProperty(key) && event.keyCode === keys[key]) {

                var iAK = activeKeys.indexOf(key);

                if (iAK > -1) {
                    activeKeys.splice(iAK, 1);
                }
            }
        }

        //execute the end callback on the active key binding
        return pruneActiveKeyBindings(event);

    });

    //bind to the window blur event and clear all pressed keys
    bind(window, "blur", function() {
        activeKeys = [];

        //execute the end callback on the active key binding
        return pruneActiveKeyBindings(event);
    });

    /**
     * Generates an array of active key bindings
     */
    function queryActiveBindings() {
        var bindingStack = [];

        //loop through the key binding groups by number of keys.
        for (var keyCount = keyBindingGroups.length; keyCount > -1; keyCount -= 1) {
            if (keyBindingGroups[keyCount]) {
                var KeyBindingGroup = keyBindingGroups[keyCount];

                //loop through the key bindings of the same key length.
                for (var bindingIndex = 0; bindingIndex < KeyBindingGroup.length; bindingIndex += 1) {
                    var binding = KeyBindingGroup[bindingIndex],

                        //assume the binding is active till a required key is found to be unsatisfied
                        keyBindingActive = true;

                    //loop through each key required by the binding.
                    for (var keyIndex = 0; keyIndex < binding.keys.length; keyIndex += 1) {
                        var key = binding.keys[keyIndex];

                        //if the current key is not in the active keys array the mark the binding as inactive
                        if (activeKeys.indexOf(key) < 0) {
                            keyBindingActive = false;
                        }
                    }

                    //if the key combo is still active then push it into the binding stack
                    if (keyBindingActive) {
                        bindingStack.push(binding);
                    }
                }
            }
        }

        return bindingStack;
    }

    /**
     * Collects active keys, sets active binds and fires on key down callbacks
     * @param event
     */
    function executeActiveKeyBindings(event) {

        if (activeKeys < 1) {
            return true;
        }

        var bindingStack = queryActiveBindings(),
            spentKeys = [],
            output;

        //loop through each active binding
        for (var bindingIndex = 0; bindingIndex < bindingStack.length; bindingIndex += 1) {
            var binding = bindingStack[bindingIndex],
                usesSpentKey = false;

            //check each of the required keys. Make sure they have not been used by another binding
            for (var keyIndex = 0; keyIndex < binding.keys.length; keyIndex += 1) {
                var key = binding.keys[keyIndex];
                if (spentKeys.indexOf(key) > -1) {
                    usesSpentKey = true;
                    break;
                }
            }

            //if the binding does not use a key that has been spent then execute it
            if (!usesSpentKey) {

                //fire the callback
                if (typeof binding.callback === "function") {
                    if (!binding.callback(event, binding.keys, binding.keyCombo)) {
                        output = false
                    }
                }

                //add the binding's combo to the active bindings array
                if (!activeBindings[binding.keyCombo]) {
                    activeBindings[binding.keyCombo] = binding;
                }

                //add the current key binding's keys to the spent keys array
                for (var keyIndex = 0; keyIndex < binding.keys.length; keyIndex += 1) {
                    var key = binding.keys[keyIndex];
                    if (spentKeys.indexOf(key) < 0) {
                        spentKeys.push(key);
                    }
                }
            }
        }

        //if there are spent keys then we know a binding was fired
        // and that we need to tell jQuery to prevent event bubbling.
        if (spentKeys.length) {
            return false;
        }

        return output;
    }

    /**
     * Removes no longer active keys and fires the on key up callbacks for associated active bindings.
     * @param event
     */
    function pruneActiveKeyBindings(event) {
        var bindingStack = queryActiveBindings();
        var output;

        //loop through the active combos
        for (var bindingCombo in activeBindings) {
            if (activeBindings.hasOwnProperty(bindingCombo)) {
                var binding = activeBindings[bindingCombo],
                    active = false;

                //loop thorugh the active bindings
                for (var bindingIndex = 0; bindingIndex < bindingStack.length; bindingIndex += 1) {
                    var activeCombo = bindingStack[bindingIndex].keyCombo;

                    //check to see if the combo is still active
                    if (activeCombo === bindingCombo) {
                        active = true;
                        break;
                    }
                }

                //if the combo is no longer active then fire its end callback and remove it
                if (!active) {

                    if (typeof binding.endCallback === "function") {
                        if (!binding.endCallback(event, binding.keys, binding.keyCombo)) {
                            output = false
                        }
                    }

                    delete activeBindings[bindingCombo];
                }
            }
        }

        return output;
    }

    /**
     * Binds a on key down and on key up callback to a key or key combo. Accepts a string containing the name of each
     * key you want to bind to comma separated. If you want to bind a combo the use the plus sign to link keys together.
     * Example: 'ctrl + x, ctrl + c' Will fire if Control and x or y are pressed at the same time.
     * @param keyCombo
     * @param callback
     * @param endCallback
     */
    function bindKey(keyCombo, callback, endCallback) {

        function clear() {
            if (keys && keys.length) {
                var keyBindingGroup = keyBindingGroups[keys.length];

                if (keyBindingGroup.indexOf(keyBinding) > -1) {
                    var index = keyBindingGroups[keys.length].indexOf(keyBinding);
                    keyBindingGroups[keys.length].splice(index, 1);
                }
            }
        }

        //create an array of combos from the first argument
        var bindSets = keyCombo.toLowerCase().replace(/\s/g, '').split(',');

        //create a binding for each key combo
        for (var i = 0; i < bindSets.length; i += 1) {

            //split up the keys
            var keys = bindSets[i].split('+');

            //if there are keys in the current combo
            if (keys.length) {
                if (!keyBindingGroups[keys.length]) { keyBindingGroups[keys.length] = []; }

                //define the
                var keyBinding = {
                    "callback": callback,
                    "endCallback": endCallback,
                    "keyCombo": bindSets[i],
                    "keys": keys
                };

                //save the binding sorted by length
                keyBindingGroups[keys.length].push(keyBinding);
            }
        }

        return {
            "clear": clear
        }
    }

    /**
     * Binds keys or key combos to an axis. The keys should be in the following order; up, down, left, right. If any
     * of the the binded key or key combos are active the callback will fire. The callback will be passed an array
     * containing two numbers. The first represents x and the second represents y. Both have a possible range of -1,
     * 0, or 1 depending on the axis direction.
     * @param up
     * @param down
     * @param left
     * @param right
     * @param callback
     */
    function bindAxis(up, down, left, right, callback) {

        function clear() {
            if (typeof clearUp === 'function') { clearUp(); }
            if (typeof clearDown === 'function') { clearDown(); }
            if (typeof clearLeft === 'function') { clearLeft(); }
            if (typeof clearRight === 'function') { clearRight(); }
            if (typeof timer === 'function') { clearInterval(timer); }
        }

        var axis = [0, 0];

        if (typeof callback !== 'function') {
            return false;
        }

        //up
        var clearUp = bindKey(up, function() {
            if (axis[0] === 0) {
                axis[0] = -1;
            }
        }, function() {
            axis[0] = 0;
        }).clear;

        //down
        var clearDown = bindKey(down, function() {
            if (axis[0] === 0) {
                axis[0] = 1;
            }
        }, function() {
            axis[0] = 0;
        }).clear;

        //left
        var clearLeft = bindKey(left, function() {
            if (axis[1] === 0) {
                axis[1] = -1;
            }
        }, function() {
            axis[1] = 0;
        }).clear;

        //right
        var clearRight = bindKey(right, function() {
            if (axis[1] === 0) {
                axis[1] = 1;
            }
        }, function() {
            axis[1] = 0;
        }).clear;

        var timer = setInterval(function() {

            //NO CHANGE
            if (axis[0] === 0 && axis[1] === 0) {
                return;
            }

            //run the callback
            callback(axis);

        }, 1);

        return {
            "clear": clear
        }
    }

    /**
     * Clears all key and key combo binds containing a given key or keys.
     * @param keys
     */
    function unbindKey(keys) {

        if (keys === 'all') {
            keyBindingGroups = [];
            return;
        }

        keys = keys.replace(/\s/g, '').split(',');

        //loop through the key binding groups.
        for (var iKCL = keyBindingGroups.length; iKCL > -1; iKCL -= 1) {
            if (keyBindingGroups[iKCL]) {
                var KeyBindingGroup = keyBindingGroups[iKCL];

                //loop through the key bindings.
                for (var iB = 0; iB < KeyBindingGroup.length; iB += 1) {
                    var keyBinding = KeyBindingGroup[iB],
                        remove = false;

                    //loop through the current key binding keys.
                    for (var iKB = 0; iKB < keyBinding.keys.length; iKB += 1) {
                        var key = keyBinding.keys[iKB];

                        //loop through the keys to be removed
                        for (var iKR = 0; iKR < keys.length; iKR += 1) {
                            var keyToRemove = keys[iKR];
                            if (keyToRemove === key) {
                                remove = true;
                                break;
                            }
                        }
                        if (remove) { break; }
                    }
                    if (remove) {
                        keyBindingGroups[iKCL].splice(iB, 1);
                        iB -= 1;
                        if (keyBindingGroups[iKCL].length < 1) {
                            delete keyBindingGroups[iKCL];
                        }
                    }
                }
            }
        }
    }

    /**
     * Gets an array of active keys
     */
    function getActiveKeys() {
        return activeKeys;
    }

    /**
     * Adds a new keyboard local not supported by keyboard JS
     * @param local
     * @param keys
     */
    function addLocale(local, keys) {
        locals[local] = keys;
    }

    /**
     * Changes the keyboard local
     * @param local
     */
    function setLocale(local) {
        if (locals[local]) {
            keys = locals[local];
        }

    }

    return {
        "bind": {
            "key": bindKey,
            "axis": bindAxis
        },
        "activeKeys": getActiveKeys,
        "unbind": {
            "key": unbindKey
        },
        "locale": {
            "add": addLocale,
            "set": setLocale
        }
    }

}));